metadata.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2012 The Python Software Foundation.
  4. # See LICENSE.txt and CONTRIBUTORS.txt.
  5. #
  6. """Implementation of the Metadata for Python packages PEPs.
  7. Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and withdrawn 2.0).
  8. """
  9. from __future__ import unicode_literals
  10. import codecs
  11. from email import message_from_file
  12. import json
  13. import logging
  14. import re
  15. from . import DistlibException, __version__
  16. from .compat import StringIO, string_types, text_type
  17. from .markers import interpret
  18. from .util import extract_by_key, get_extras
  19. from .version import get_scheme, PEP440_VERSION_RE
  20. logger = logging.getLogger(__name__)
  21. class MetadataMissingError(DistlibException):
  22. """A required metadata is missing"""
  23. class MetadataConflictError(DistlibException):
  24. """Attempt to read or write metadata fields that are conflictual."""
  25. class MetadataUnrecognizedVersionError(DistlibException):
  26. """Unknown metadata version number."""
  27. class MetadataInvalidError(DistlibException):
  28. """A metadata value is invalid"""
  29. # public API of this module
  30. __all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION']
  31. # Encoding used for the PKG-INFO files
  32. PKG_INFO_ENCODING = 'utf-8'
  33. # preferred version. Hopefully will be changed
  34. # to 1.2 once PEP 345 is supported everywhere
  35. PKG_INFO_PREFERRED_VERSION = '1.1'
  36. _LINE_PREFIX_1_2 = re.compile('\n \\|')
  37. _LINE_PREFIX_PRE_1_2 = re.compile('\n ')
  38. _241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  39. 'Summary', 'Description',
  40. 'Keywords', 'Home-page', 'Author', 'Author-email',
  41. 'License')
  42. _314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  43. 'Supported-Platform', 'Summary', 'Description',
  44. 'Keywords', 'Home-page', 'Author', 'Author-email',
  45. 'License', 'Classifier', 'Download-URL', 'Obsoletes',
  46. 'Provides', 'Requires')
  47. _314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier',
  48. 'Download-URL')
  49. _345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  50. 'Supported-Platform', 'Summary', 'Description',
  51. 'Keywords', 'Home-page', 'Author', 'Author-email',
  52. 'Maintainer', 'Maintainer-email', 'License',
  53. 'Classifier', 'Download-URL', 'Obsoletes-Dist',
  54. 'Project-URL', 'Provides-Dist', 'Requires-Dist',
  55. 'Requires-Python', 'Requires-External')
  56. _345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python',
  57. 'Obsoletes-Dist', 'Requires-External', 'Maintainer',
  58. 'Maintainer-email', 'Project-URL')
  59. _426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  60. 'Supported-Platform', 'Summary', 'Description',
  61. 'Keywords', 'Home-page', 'Author', 'Author-email',
  62. 'Maintainer', 'Maintainer-email', 'License',
  63. 'Classifier', 'Download-URL', 'Obsoletes-Dist',
  64. 'Project-URL', 'Provides-Dist', 'Requires-Dist',
  65. 'Requires-Python', 'Requires-External', 'Private-Version',
  66. 'Obsoleted-By', 'Setup-Requires-Dist', 'Extension',
  67. 'Provides-Extra')
  68. _426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
  69. 'Setup-Requires-Dist', 'Extension')
  70. # See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in
  71. # the metadata. Include them in the tuple literal below to allow them
  72. # (for now).
  73. _566_FIELDS = _426_FIELDS + ('Description-Content-Type',
  74. 'Requires', 'Provides')
  75. _566_MARKERS = ('Description-Content-Type',)
  76. _ALL_FIELDS = set()
  77. _ALL_FIELDS.update(_241_FIELDS)
  78. _ALL_FIELDS.update(_314_FIELDS)
  79. _ALL_FIELDS.update(_345_FIELDS)
  80. _ALL_FIELDS.update(_426_FIELDS)
  81. _ALL_FIELDS.update(_566_FIELDS)
  82. EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
  83. def _version2fieldlist(version):
  84. if version == '1.0':
  85. return _241_FIELDS
  86. elif version == '1.1':
  87. return _314_FIELDS
  88. elif version == '1.2':
  89. return _345_FIELDS
  90. elif version in ('1.3', '2.1'):
  91. return _345_FIELDS + _566_FIELDS
  92. elif version == '2.0':
  93. return _426_FIELDS
  94. raise MetadataUnrecognizedVersionError(version)
  95. def _best_version(fields):
  96. """Detect the best version depending on the fields used."""
  97. def _has_marker(keys, markers):
  98. for marker in markers:
  99. if marker in keys:
  100. return True
  101. return False
  102. keys = []
  103. for key, value in fields.items():
  104. if value in ([], 'UNKNOWN', None):
  105. continue
  106. keys.append(key)
  107. possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.0', '2.1']
  108. # first let's try to see if a field is not part of one of the version
  109. for key in keys:
  110. if key not in _241_FIELDS and '1.0' in possible_versions:
  111. possible_versions.remove('1.0')
  112. logger.debug('Removed 1.0 due to %s', key)
  113. if key not in _314_FIELDS and '1.1' in possible_versions:
  114. possible_versions.remove('1.1')
  115. logger.debug('Removed 1.1 due to %s', key)
  116. if key not in _345_FIELDS and '1.2' in possible_versions:
  117. possible_versions.remove('1.2')
  118. logger.debug('Removed 1.2 due to %s', key)
  119. if key not in _566_FIELDS and '1.3' in possible_versions:
  120. possible_versions.remove('1.3')
  121. logger.debug('Removed 1.3 due to %s', key)
  122. if key not in _566_FIELDS and '2.1' in possible_versions:
  123. if key != 'Description': # In 2.1, description allowed after headers
  124. possible_versions.remove('2.1')
  125. logger.debug('Removed 2.1 due to %s', key)
  126. if key not in _426_FIELDS and '2.0' in possible_versions:
  127. possible_versions.remove('2.0')
  128. logger.debug('Removed 2.0 due to %s', key)
  129. # possible_version contains qualified versions
  130. if len(possible_versions) == 1:
  131. return possible_versions[0] # found !
  132. elif len(possible_versions) == 0:
  133. logger.debug('Out of options - unknown metadata set: %s', fields)
  134. raise MetadataConflictError('Unknown metadata set')
  135. # let's see if one unique marker is found
  136. is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)
  137. is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)
  138. is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
  139. is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
  140. if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_0) > 1:
  141. raise MetadataConflictError('You used incompatible 1.1/1.2/2.0/2.1 fields')
  142. # we have the choice, 1.0, or 1.2, or 2.0
  143. # - 1.0 has a broken Summary field but works with all tools
  144. # - 1.1 is to avoid
  145. # - 1.2 fixes Summary but has little adoption
  146. # - 2.0 adds more features and is very new
  147. if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_0:
  148. # we couldn't find any specific marker
  149. if PKG_INFO_PREFERRED_VERSION in possible_versions:
  150. return PKG_INFO_PREFERRED_VERSION
  151. if is_1_1:
  152. return '1.1'
  153. if is_1_2:
  154. return '1.2'
  155. if is_2_1:
  156. return '2.1'
  157. return '2.0'
  158. # This follows the rules about transforming keys as described in
  159. # https://www.python.org/dev/peps/pep-0566/#id17
  160. _ATTR2FIELD = {
  161. name.lower().replace("-", "_"): name for name in _ALL_FIELDS
  162. }
  163. _FIELD2ATTR = {field: attr for attr, field in _ATTR2FIELD.items()}
  164. _PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')
  165. _VERSIONS_FIELDS = ('Requires-Python',)
  166. _VERSION_FIELDS = ('Version',)
  167. _LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
  168. 'Requires', 'Provides', 'Obsoletes-Dist',
  169. 'Provides-Dist', 'Requires-Dist', 'Requires-External',
  170. 'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
  171. 'Provides-Extra', 'Extension')
  172. _LISTTUPLEFIELDS = ('Project-URL',)
  173. _ELEMENTSFIELD = ('Keywords',)
  174. _UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
  175. _MISSING = object()
  176. _FILESAFE = re.compile('[^A-Za-z0-9.]+')
  177. def _get_name_and_version(name, version, for_filename=False):
  178. """Return the distribution name with version.
  179. If for_filename is true, return a filename-escaped form."""
  180. if for_filename:
  181. # For both name and version any runs of non-alphanumeric or '.'
  182. # characters are replaced with a single '-'. Additionally any
  183. # spaces in the version string become '.'
  184. name = _FILESAFE.sub('-', name)
  185. version = _FILESAFE.sub('-', version.replace(' ', '.'))
  186. return '%s-%s' % (name, version)
  187. class LegacyMetadata(object):
  188. """The legacy metadata of a release.
  189. Supports versions 1.0, 1.1, 1.2, 2.0 and 1.3/2.1 (auto-detected). You can
  190. instantiate the class with one of these arguments (or none):
  191. - *path*, the path to a metadata file
  192. - *fileobj* give a file-like object with metadata as content
  193. - *mapping* is a dict-like object
  194. - *scheme* is a version scheme name
  195. """
  196. # TODO document the mapping API and UNKNOWN default key
  197. def __init__(self, path=None, fileobj=None, mapping=None,
  198. scheme='default'):
  199. if [path, fileobj, mapping].count(None) < 2:
  200. raise TypeError('path, fileobj and mapping are exclusive')
  201. self._fields = {}
  202. self.requires_files = []
  203. self._dependencies = None
  204. self.scheme = scheme
  205. if path is not None:
  206. self.read(path)
  207. elif fileobj is not None:
  208. self.read_file(fileobj)
  209. elif mapping is not None:
  210. self.update(mapping)
  211. self.set_metadata_version()
  212. def set_metadata_version(self):
  213. self._fields['Metadata-Version'] = _best_version(self._fields)
  214. def _write_field(self, fileobj, name, value):
  215. fileobj.write('%s: %s\n' % (name, value))
  216. def __getitem__(self, name):
  217. return self.get(name)
  218. def __setitem__(self, name, value):
  219. return self.set(name, value)
  220. def __delitem__(self, name):
  221. field_name = self._convert_name(name)
  222. try:
  223. del self._fields[field_name]
  224. except KeyError:
  225. raise KeyError(name)
  226. def __contains__(self, name):
  227. return (name in self._fields or
  228. self._convert_name(name) in self._fields)
  229. def _convert_name(self, name):
  230. if name in _ALL_FIELDS:
  231. return name
  232. name = name.replace('-', '_').lower()
  233. return _ATTR2FIELD.get(name, name)
  234. def _default_value(self, name):
  235. if name in _LISTFIELDS or name in _ELEMENTSFIELD:
  236. return []
  237. return 'UNKNOWN'
  238. def _remove_line_prefix(self, value):
  239. if self.metadata_version in ('1.0', '1.1'):
  240. return _LINE_PREFIX_PRE_1_2.sub('\n', value)
  241. else:
  242. return _LINE_PREFIX_1_2.sub('\n', value)
  243. def __getattr__(self, name):
  244. if name in _ATTR2FIELD:
  245. return self[name]
  246. raise AttributeError(name)
  247. #
  248. # Public API
  249. #
  250. # dependencies = property(_get_dependencies, _set_dependencies)
  251. def get_fullname(self, filesafe=False):
  252. """Return the distribution name with version.
  253. If filesafe is true, return a filename-escaped form."""
  254. return _get_name_and_version(self['Name'], self['Version'], filesafe)
  255. def is_field(self, name):
  256. """return True if name is a valid metadata key"""
  257. name = self._convert_name(name)
  258. return name in _ALL_FIELDS
  259. def is_multi_field(self, name):
  260. name = self._convert_name(name)
  261. return name in _LISTFIELDS
  262. def read(self, filepath):
  263. """Read the metadata values from a file path."""
  264. fp = codecs.open(filepath, 'r', encoding='utf-8')
  265. try:
  266. self.read_file(fp)
  267. finally:
  268. fp.close()
  269. def read_file(self, fileob):
  270. """Read the metadata values from a file object."""
  271. msg = message_from_file(fileob)
  272. self._fields['Metadata-Version'] = msg['metadata-version']
  273. # When reading, get all the fields we can
  274. for field in _ALL_FIELDS:
  275. if field not in msg:
  276. continue
  277. if field in _LISTFIELDS:
  278. # we can have multiple lines
  279. values = msg.get_all(field)
  280. if field in _LISTTUPLEFIELDS and values is not None:
  281. values = [tuple(value.split(',')) for value in values]
  282. self.set(field, values)
  283. else:
  284. # single line
  285. value = msg[field]
  286. if value is not None and value != 'UNKNOWN':
  287. self.set(field, value)
  288. # PEP 566 specifies that the body be used for the description, if
  289. # available
  290. body = msg.get_payload()
  291. self["Description"] = body if body else self["Description"]
  292. # logger.debug('Attempting to set metadata for %s', self)
  293. # self.set_metadata_version()
  294. def write(self, filepath, skip_unknown=False):
  295. """Write the metadata fields to filepath."""
  296. fp = codecs.open(filepath, 'w', encoding='utf-8')
  297. try:
  298. self.write_file(fp, skip_unknown)
  299. finally:
  300. fp.close()
  301. def write_file(self, fileobject, skip_unknown=False):
  302. """Write the PKG-INFO format data to a file object."""
  303. self.set_metadata_version()
  304. for field in _version2fieldlist(self['Metadata-Version']):
  305. values = self.get(field)
  306. if skip_unknown and values in ('UNKNOWN', [], ['UNKNOWN']):
  307. continue
  308. if field in _ELEMENTSFIELD:
  309. self._write_field(fileobject, field, ','.join(values))
  310. continue
  311. if field not in _LISTFIELDS:
  312. if field == 'Description':
  313. if self.metadata_version in ('1.0', '1.1'):
  314. values = values.replace('\n', '\n ')
  315. else:
  316. values = values.replace('\n', '\n |')
  317. values = [values]
  318. if field in _LISTTUPLEFIELDS:
  319. values = [','.join(value) for value in values]
  320. for value in values:
  321. self._write_field(fileobject, field, value)
  322. def update(self, other=None, **kwargs):
  323. """Set metadata values from the given iterable `other` and kwargs.
  324. Behavior is like `dict.update`: If `other` has a ``keys`` method,
  325. they are looped over and ``self[key]`` is assigned ``other[key]``.
  326. Else, ``other`` is an iterable of ``(key, value)`` iterables.
  327. Keys that don't match a metadata field or that have an empty value are
  328. dropped.
  329. """
  330. def _set(key, value):
  331. if key in _ATTR2FIELD and value:
  332. self.set(self._convert_name(key), value)
  333. if not other:
  334. # other is None or empty container
  335. pass
  336. elif hasattr(other, 'keys'):
  337. for k in other.keys():
  338. _set(k, other[k])
  339. else:
  340. for k, v in other:
  341. _set(k, v)
  342. if kwargs:
  343. for k, v in kwargs.items():
  344. _set(k, v)
  345. def set(self, name, value):
  346. """Control then set a metadata field."""
  347. name = self._convert_name(name)
  348. if ((name in _ELEMENTSFIELD or name == 'Platform') and
  349. not isinstance(value, (list, tuple))):
  350. if isinstance(value, string_types):
  351. value = [v.strip() for v in value.split(',')]
  352. else:
  353. value = []
  354. elif (name in _LISTFIELDS and
  355. not isinstance(value, (list, tuple))):
  356. if isinstance(value, string_types):
  357. value = [value]
  358. else:
  359. value = []
  360. if logger.isEnabledFor(logging.WARNING):
  361. project_name = self['Name']
  362. scheme = get_scheme(self.scheme)
  363. if name in _PREDICATE_FIELDS and value is not None:
  364. for v in value:
  365. # check that the values are valid
  366. if not scheme.is_valid_matcher(v.split(';')[0]):
  367. logger.warning(
  368. "'%s': '%s' is not valid (field '%s')",
  369. project_name, v, name)
  370. # FIXME this rejects UNKNOWN, is that right?
  371. elif name in _VERSIONS_FIELDS and value is not None:
  372. if not scheme.is_valid_constraint_list(value):
  373. logger.warning("'%s': '%s' is not a valid version (field '%s')",
  374. project_name, value, name)
  375. elif name in _VERSION_FIELDS and value is not None:
  376. if not scheme.is_valid_version(value):
  377. logger.warning("'%s': '%s' is not a valid version (field '%s')",
  378. project_name, value, name)
  379. if name in _UNICODEFIELDS:
  380. if name == 'Description':
  381. value = self._remove_line_prefix(value)
  382. self._fields[name] = value
  383. def get(self, name, default=_MISSING):
  384. """Get a metadata field."""
  385. name = self._convert_name(name)
  386. if name not in self._fields:
  387. if default is _MISSING:
  388. default = self._default_value(name)
  389. return default
  390. if name in _UNICODEFIELDS:
  391. value = self._fields[name]
  392. return value
  393. elif name in _LISTFIELDS:
  394. value = self._fields[name]
  395. if value is None:
  396. return []
  397. res = []
  398. for val in value:
  399. if name not in _LISTTUPLEFIELDS:
  400. res.append(val)
  401. else:
  402. # That's for Project-URL
  403. res.append((val[0], val[1]))
  404. return res
  405. elif name in _ELEMENTSFIELD:
  406. value = self._fields[name]
  407. if isinstance(value, string_types):
  408. return value.split(',')
  409. return self._fields[name]
  410. def check(self, strict=False):
  411. """Check if the metadata is compliant. If strict is True then raise if
  412. no Name or Version are provided"""
  413. self.set_metadata_version()
  414. # XXX should check the versions (if the file was loaded)
  415. missing, warnings = [], []
  416. for attr in ('Name', 'Version'): # required by PEP 345
  417. if attr not in self:
  418. missing.append(attr)
  419. if strict and missing != []:
  420. msg = 'missing required metadata: %s' % ', '.join(missing)
  421. raise MetadataMissingError(msg)
  422. for attr in ('Home-page', 'Author'):
  423. if attr not in self:
  424. missing.append(attr)
  425. # checking metadata 1.2 (XXX needs to check 1.1, 1.0)
  426. if self['Metadata-Version'] != '1.2':
  427. return missing, warnings
  428. scheme = get_scheme(self.scheme)
  429. def are_valid_constraints(value):
  430. for v in value:
  431. if not scheme.is_valid_matcher(v.split(';')[0]):
  432. return False
  433. return True
  434. for fields, controller in ((_PREDICATE_FIELDS, are_valid_constraints),
  435. (_VERSIONS_FIELDS,
  436. scheme.is_valid_constraint_list),
  437. (_VERSION_FIELDS,
  438. scheme.is_valid_version)):
  439. for field in fields:
  440. value = self.get(field, None)
  441. if value is not None and not controller(value):
  442. warnings.append("Wrong value for '%s': %s" % (field, value))
  443. return missing, warnings
  444. def todict(self, skip_missing=False):
  445. """Return fields as a dict.
  446. Field names will be converted to use the underscore-lowercase style
  447. instead of hyphen-mixed case (i.e. home_page instead of Home-page).
  448. This is as per https://www.python.org/dev/peps/pep-0566/#id17.
  449. """
  450. self.set_metadata_version()
  451. fields = _version2fieldlist(self['Metadata-Version'])
  452. data = {}
  453. for field_name in fields:
  454. if not skip_missing or field_name in self._fields:
  455. key = _FIELD2ATTR[field_name]
  456. if key != 'project_url':
  457. data[key] = self[field_name]
  458. else:
  459. data[key] = [','.join(u) for u in self[field_name]]
  460. return data
  461. def add_requirements(self, requirements):
  462. if self['Metadata-Version'] == '1.1':
  463. # we can't have 1.1 metadata *and* Setuptools requires
  464. for field in ('Obsoletes', 'Requires', 'Provides'):
  465. if field in self:
  466. del self[field]
  467. self['Requires-Dist'] += requirements
  468. # Mapping API
  469. # TODO could add iter* variants
  470. def keys(self):
  471. return list(_version2fieldlist(self['Metadata-Version']))
  472. def __iter__(self):
  473. for key in self.keys():
  474. yield key
  475. def values(self):
  476. return [self[key] for key in self.keys()]
  477. def items(self):
  478. return [(key, self[key]) for key in self.keys()]
  479. def __repr__(self):
  480. return '<%s %s %s>' % (self.__class__.__name__, self.name,
  481. self.version)
  482. METADATA_FILENAME = 'pydist.json'
  483. WHEEL_METADATA_FILENAME = 'metadata.json'
  484. LEGACY_METADATA_FILENAME = 'METADATA'
  485. class Metadata(object):
  486. """
  487. The metadata of a release. This implementation uses 2.0 (JSON)
  488. metadata where possible. If not possible, it wraps a LegacyMetadata
  489. instance which handles the key-value metadata format.
  490. """
  491. METADATA_VERSION_MATCHER = re.compile(r'^\d+(\.\d+)*$')
  492. NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
  493. VERSION_MATCHER = PEP440_VERSION_RE
  494. SUMMARY_MATCHER = re.compile('.{1,2047}')
  495. METADATA_VERSION = '2.0'
  496. GENERATOR = 'distlib (%s)' % __version__
  497. MANDATORY_KEYS = {
  498. 'name': (),
  499. 'version': (),
  500. 'summary': ('legacy',),
  501. }
  502. INDEX_KEYS = ('name version license summary description author '
  503. 'author_email keywords platform home_page classifiers '
  504. 'download_url')
  505. DEPENDENCY_KEYS = ('extras run_requires test_requires build_requires '
  506. 'dev_requires provides meta_requires obsoleted_by '
  507. 'supports_environments')
  508. SYNTAX_VALIDATORS = {
  509. 'metadata_version': (METADATA_VERSION_MATCHER, ()),
  510. 'name': (NAME_MATCHER, ('legacy',)),
  511. 'version': (VERSION_MATCHER, ('legacy',)),
  512. 'summary': (SUMMARY_MATCHER, ('legacy',)),
  513. }
  514. __slots__ = ('_legacy', '_data', 'scheme')
  515. def __init__(self, path=None, fileobj=None, mapping=None,
  516. scheme='default'):
  517. if [path, fileobj, mapping].count(None) < 2:
  518. raise TypeError('path, fileobj and mapping are exclusive')
  519. self._legacy = None
  520. self._data = None
  521. self.scheme = scheme
  522. #import pdb; pdb.set_trace()
  523. if mapping is not None:
  524. try:
  525. self._validate_mapping(mapping, scheme)
  526. self._data = mapping
  527. except MetadataUnrecognizedVersionError:
  528. self._legacy = LegacyMetadata(mapping=mapping, scheme=scheme)
  529. self.validate()
  530. else:
  531. data = None
  532. if path:
  533. with open(path, 'rb') as f:
  534. data = f.read()
  535. elif fileobj:
  536. data = fileobj.read()
  537. if data is None:
  538. # Initialised with no args - to be added
  539. self._data = {
  540. 'metadata_version': self.METADATA_VERSION,
  541. 'generator': self.GENERATOR,
  542. }
  543. else:
  544. if not isinstance(data, text_type):
  545. data = data.decode('utf-8')
  546. try:
  547. self._data = json.loads(data)
  548. self._validate_mapping(self._data, scheme)
  549. except ValueError:
  550. # Note: MetadataUnrecognizedVersionError does not
  551. # inherit from ValueError (it's a DistlibException,
  552. # which should not inherit from ValueError).
  553. # The ValueError comes from the json.load - if that
  554. # succeeds and we get a validation error, we want
  555. # that to propagate
  556. self._legacy = LegacyMetadata(fileobj=StringIO(data),
  557. scheme=scheme)
  558. self.validate()
  559. common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))
  560. none_list = (None, list)
  561. none_dict = (None, dict)
  562. mapped_keys = {
  563. 'run_requires': ('Requires-Dist', list),
  564. 'build_requires': ('Setup-Requires-Dist', list),
  565. 'dev_requires': none_list,
  566. 'test_requires': none_list,
  567. 'meta_requires': none_list,
  568. 'extras': ('Provides-Extra', list),
  569. 'modules': none_list,
  570. 'namespaces': none_list,
  571. 'exports': none_dict,
  572. 'commands': none_dict,
  573. 'classifiers': ('Classifier', list),
  574. 'source_url': ('Download-URL', None),
  575. 'metadata_version': ('Metadata-Version', None),
  576. }
  577. del none_list, none_dict
  578. def __getattribute__(self, key):
  579. common = object.__getattribute__(self, 'common_keys')
  580. mapped = object.__getattribute__(self, 'mapped_keys')
  581. if key in mapped:
  582. lk, maker = mapped[key]
  583. if self._legacy:
  584. if lk is None:
  585. result = None if maker is None else maker()
  586. else:
  587. result = self._legacy.get(lk)
  588. else:
  589. value = None if maker is None else maker()
  590. if key not in ('commands', 'exports', 'modules', 'namespaces',
  591. 'classifiers'):
  592. result = self._data.get(key, value)
  593. else:
  594. # special cases for PEP 459
  595. sentinel = object()
  596. result = sentinel
  597. d = self._data.get('extensions')
  598. if d:
  599. if key == 'commands':
  600. result = d.get('python.commands', value)
  601. elif key == 'classifiers':
  602. d = d.get('python.details')
  603. if d:
  604. result = d.get(key, value)
  605. else:
  606. d = d.get('python.exports')
  607. if not d:
  608. d = self._data.get('python.exports')
  609. if d:
  610. result = d.get(key, value)
  611. if result is sentinel:
  612. result = value
  613. elif key not in common:
  614. result = object.__getattribute__(self, key)
  615. elif self._legacy:
  616. result = self._legacy.get(key)
  617. else:
  618. result = self._data.get(key)
  619. return result
  620. def _validate_value(self, key, value, scheme=None):
  621. if key in self.SYNTAX_VALIDATORS:
  622. pattern, exclusions = self.SYNTAX_VALIDATORS[key]
  623. if (scheme or self.scheme) not in exclusions:
  624. m = pattern.match(value)
  625. if not m:
  626. raise MetadataInvalidError("'%s' is an invalid value for "
  627. "the '%s' property" % (value,
  628. key))
  629. def __setattr__(self, key, value):
  630. self._validate_value(key, value)
  631. common = object.__getattribute__(self, 'common_keys')
  632. mapped = object.__getattribute__(self, 'mapped_keys')
  633. if key in mapped:
  634. lk, _ = mapped[key]
  635. if self._legacy:
  636. if lk is None:
  637. raise NotImplementedError
  638. self._legacy[lk] = value
  639. elif key not in ('commands', 'exports', 'modules', 'namespaces',
  640. 'classifiers'):
  641. self._data[key] = value
  642. else:
  643. # special cases for PEP 459
  644. d = self._data.setdefault('extensions', {})
  645. if key == 'commands':
  646. d['python.commands'] = value
  647. elif key == 'classifiers':
  648. d = d.setdefault('python.details', {})
  649. d[key] = value
  650. else:
  651. d = d.setdefault('python.exports', {})
  652. d[key] = value
  653. elif key not in common:
  654. object.__setattr__(self, key, value)
  655. else:
  656. if key == 'keywords':
  657. if isinstance(value, string_types):
  658. value = value.strip()
  659. if value:
  660. value = value.split()
  661. else:
  662. value = []
  663. if self._legacy:
  664. self._legacy[key] = value
  665. else:
  666. self._data[key] = value
  667. @property
  668. def name_and_version(self):
  669. return _get_name_and_version(self.name, self.version, True)
  670. @property
  671. def provides(self):
  672. if self._legacy:
  673. result = self._legacy['Provides-Dist']
  674. else:
  675. result = self._data.setdefault('provides', [])
  676. s = '%s (%s)' % (self.name, self.version)
  677. if s not in result:
  678. result.append(s)
  679. return result
  680. @provides.setter
  681. def provides(self, value):
  682. if self._legacy:
  683. self._legacy['Provides-Dist'] = value
  684. else:
  685. self._data['provides'] = value
  686. def get_requirements(self, reqts, extras=None, env=None):
  687. """
  688. Base method to get dependencies, given a set of extras
  689. to satisfy and an optional environment context.
  690. :param reqts: A list of sometimes-wanted dependencies,
  691. perhaps dependent on extras and environment.
  692. :param extras: A list of optional components being requested.
  693. :param env: An optional environment for marker evaluation.
  694. """
  695. if self._legacy:
  696. result = reqts
  697. else:
  698. result = []
  699. extras = get_extras(extras or [], self.extras)
  700. for d in reqts:
  701. if 'extra' not in d and 'environment' not in d:
  702. # unconditional
  703. include = True
  704. else:
  705. if 'extra' not in d:
  706. # Not extra-dependent - only environment-dependent
  707. include = True
  708. else:
  709. include = d.get('extra') in extras
  710. if include:
  711. # Not excluded because of extras, check environment
  712. marker = d.get('environment')
  713. if marker:
  714. include = interpret(marker, env)
  715. if include:
  716. result.extend(d['requires'])
  717. for key in ('build', 'dev', 'test'):
  718. e = ':%s:' % key
  719. if e in extras:
  720. extras.remove(e)
  721. # A recursive call, but it should terminate since 'test'
  722. # has been removed from the extras
  723. reqts = self._data.get('%s_requires' % key, [])
  724. result.extend(self.get_requirements(reqts, extras=extras,
  725. env=env))
  726. return result
  727. @property
  728. def dictionary(self):
  729. if self._legacy:
  730. return self._from_legacy()
  731. return self._data
  732. @property
  733. def dependencies(self):
  734. if self._legacy:
  735. raise NotImplementedError
  736. else:
  737. return extract_by_key(self._data, self.DEPENDENCY_KEYS)
  738. @dependencies.setter
  739. def dependencies(self, value):
  740. if self._legacy:
  741. raise NotImplementedError
  742. else:
  743. self._data.update(value)
  744. def _validate_mapping(self, mapping, scheme):
  745. if mapping.get('metadata_version') != self.METADATA_VERSION:
  746. raise MetadataUnrecognizedVersionError()
  747. missing = []
  748. for key, exclusions in self.MANDATORY_KEYS.items():
  749. if key not in mapping:
  750. if scheme not in exclusions:
  751. missing.append(key)
  752. if missing:
  753. msg = 'Missing metadata items: %s' % ', '.join(missing)
  754. raise MetadataMissingError(msg)
  755. for k, v in mapping.items():
  756. self._validate_value(k, v, scheme)
  757. def validate(self):
  758. if self._legacy:
  759. missing, warnings = self._legacy.check(True)
  760. if missing or warnings:
  761. logger.warning('Metadata: missing: %s, warnings: %s',
  762. missing, warnings)
  763. else:
  764. self._validate_mapping(self._data, self.scheme)
  765. def todict(self):
  766. if self._legacy:
  767. return self._legacy.todict(True)
  768. else:
  769. result = extract_by_key(self._data, self.INDEX_KEYS)
  770. return result
  771. def _from_legacy(self):
  772. assert self._legacy and not self._data
  773. result = {
  774. 'metadata_version': self.METADATA_VERSION,
  775. 'generator': self.GENERATOR,
  776. }
  777. lmd = self._legacy.todict(True) # skip missing ones
  778. for k in ('name', 'version', 'license', 'summary', 'description',
  779. 'classifier'):
  780. if k in lmd:
  781. if k == 'classifier':
  782. nk = 'classifiers'
  783. else:
  784. nk = k
  785. result[nk] = lmd[k]
  786. kw = lmd.get('Keywords', [])
  787. if kw == ['']:
  788. kw = []
  789. result['keywords'] = kw
  790. keys = (('requires_dist', 'run_requires'),
  791. ('setup_requires_dist', 'build_requires'))
  792. for ok, nk in keys:
  793. if ok in lmd and lmd[ok]:
  794. result[nk] = [{'requires': lmd[ok]}]
  795. result['provides'] = self.provides
  796. author = {}
  797. maintainer = {}
  798. return result
  799. LEGACY_MAPPING = {
  800. 'name': 'Name',
  801. 'version': 'Version',
  802. ('extensions', 'python.details', 'license'): 'License',
  803. 'summary': 'Summary',
  804. 'description': 'Description',
  805. ('extensions', 'python.project', 'project_urls', 'Home'): 'Home-page',
  806. ('extensions', 'python.project', 'contacts', 0, 'name'): 'Author',
  807. ('extensions', 'python.project', 'contacts', 0, 'email'): 'Author-email',
  808. 'source_url': 'Download-URL',
  809. ('extensions', 'python.details', 'classifiers'): 'Classifier',
  810. }
  811. def _to_legacy(self):
  812. def process_entries(entries):
  813. reqts = set()
  814. for e in entries:
  815. extra = e.get('extra')
  816. env = e.get('environment')
  817. rlist = e['requires']
  818. for r in rlist:
  819. if not env and not extra:
  820. reqts.add(r)
  821. else:
  822. marker = ''
  823. if extra:
  824. marker = 'extra == "%s"' % extra
  825. if env:
  826. if marker:
  827. marker = '(%s) and %s' % (env, marker)
  828. else:
  829. marker = env
  830. reqts.add(';'.join((r, marker)))
  831. return reqts
  832. assert self._data and not self._legacy
  833. result = LegacyMetadata()
  834. nmd = self._data
  835. # import pdb; pdb.set_trace()
  836. for nk, ok in self.LEGACY_MAPPING.items():
  837. if not isinstance(nk, tuple):
  838. if nk in nmd:
  839. result[ok] = nmd[nk]
  840. else:
  841. d = nmd
  842. found = True
  843. for k in nk:
  844. try:
  845. d = d[k]
  846. except (KeyError, IndexError):
  847. found = False
  848. break
  849. if found:
  850. result[ok] = d
  851. r1 = process_entries(self.run_requires + self.meta_requires)
  852. r2 = process_entries(self.build_requires + self.dev_requires)
  853. if self.extras:
  854. result['Provides-Extra'] = sorted(self.extras)
  855. result['Requires-Dist'] = sorted(r1)
  856. result['Setup-Requires-Dist'] = sorted(r2)
  857. # TODO: any other fields wanted
  858. return result
  859. def write(self, path=None, fileobj=None, legacy=False, skip_unknown=True):
  860. if [path, fileobj].count(None) != 1:
  861. raise ValueError('Exactly one of path and fileobj is needed')
  862. self.validate()
  863. if legacy:
  864. if self._legacy:
  865. legacy_md = self._legacy
  866. else:
  867. legacy_md = self._to_legacy()
  868. if path:
  869. legacy_md.write(path, skip_unknown=skip_unknown)
  870. else:
  871. legacy_md.write_file(fileobj, skip_unknown=skip_unknown)
  872. else:
  873. if self._legacy:
  874. d = self._from_legacy()
  875. else:
  876. d = self._data
  877. if fileobj:
  878. json.dump(d, fileobj, ensure_ascii=True, indent=2,
  879. sort_keys=True)
  880. else:
  881. with codecs.open(path, 'w', 'utf-8') as f:
  882. json.dump(d, f, ensure_ascii=True, indent=2,
  883. sort_keys=True)
  884. def add_requirements(self, requirements):
  885. if self._legacy:
  886. self._legacy.add_requirements(requirements)
  887. else:
  888. run_requires = self._data.setdefault('run_requires', [])
  889. always = None
  890. for entry in run_requires:
  891. if 'environment' not in entry and 'extra' not in entry:
  892. always = entry
  893. break
  894. if always is None:
  895. always = { 'requires': requirements }
  896. run_requires.insert(0, always)
  897. else:
  898. rset = set(always['requires']) | set(requirements)
  899. always['requires'] = sorted(rset)
  900. def __repr__(self):
  901. name = self.name or '(no name)'
  902. version = self.version or 'no version'
  903. return '<%s %s %s (%s)>' % (self.__class__.__name__,
  904. self.metadata_version, name, version)